﻿\subsection[CPU 特权级检查及迁移]{CPU 特权级检查及迁移 \protect\footnote{
    x86 体系结构中特权级数值越小，相应的特权级别越高。
    为避免混淆，下文中涉及到特权级比较时，对语义上的等级高低进行区分时使用“高”“低”进行表述，对于数值上的大小关系，则使用“大”“小”进行表述。
}}

中断和异常都涉及到改变 CPU 的指令执行流，使执行流切换到中断例程所在的段。
保护模式下，所有的指令、数据都从属于某个段。
在访问某个段之前，CPU 会先进行对段的特权级检查，以阻止非法的内存访问。
x86 中的特权级分为 0 \TO 3 四级，数值越低代表等级越高。
在 CPU 运行时，特权级有如下三种表现形式：

\begin{itemize}
    \item 当前特权级 $CPL$（Current Privilege Level）：当前指令执行流（即 \lstinline{CS} 指向的代码段）的特权级，保存在 \lstinline{CS[1:0]} 及 \lstinline{SS[1:0]} 中。

    \item 请求特权级 $RPL$（Request Privilege Level）：当前指令请求访问某个段时使用的特权级（当前的代码段可自降身份，以低于当前特权级的等级访问某个段），保存在段选择子中的低两位。

    \item 段特权级 $DPL$（Descriptor Privilege Level）：段描述符所描述的段的特权级。

\end{itemize}

x86 中段的属性记录在段描述符中，描述符中的 \lstinline{S} 位置 $0$ 表示系统段，置 $1$ 表示非系统段。
非系统段被严格分为数据段和代码段两类，在 \lstinline{Type} 字段中予以区分。
例如，\lstinline{CS} 中引用的段类型就属于代码段，\lstinline{DS} 中引用的段类型就属于数据段。
对数据段和代码段这两种非系统段进行访问，对应地就产生 x86 体系结构中两种不同的非系统段特权级检查规则。

\subsubsection{访问数据段时的特权级检查}

对数据段进行访问总是通过一条指令（如 \lstinline{mov eax, [es:edx]}）来完成，指令请求的数据存放在某个数据段中。
对于非一致性数据段而言，必须要满足 $DPL \ge RPL \ge CPL$。
请求特权级会覆盖当前特权级，因此，即使当前特权级高于段特权级，如果请求特权级低于段特权级，对数据段的访问也会被拒绝，并抛出异常。

由于数据段本身仅包含数据，并不涉及程序执行流的改变，因此访问数据段并不会造成特权级的迁移。

\subsubsection{访问代码段时的特权级检查和迁移}

程序运行时对某个代码段进行访问，实际就是转而执行某个代码段中的指令，本质上是改变程序执行流。
因此除了访问段时的特权级检查，还有可能因为执行流转移造成特权级的迁移。
访问代码段根据访问方式不同划分为直接访问和间接访问两种方式。

直接访问代码段即使用转移指令转移程序执行流，由于近转移指令在当前代码段内进行，因此无需特权级检查。

而远转移指令（如 \lstinline{jmpf}、\lstinline{callf}）请求将执行流转移到到另一个代码段时，由于使用段选择子发起段的转移请求，涉及到段的转移，故需要在转移前检查特权级是否合法。

x86 体系结构下规定，对于非一致性代码段，必须满足 $CPL = DPL \ge RPL$。
请求特权级对于非一致性实际特权级检查无影响，但是体系结构规定请求特权级不能低于当前特权级。
对于一致性代码段，则要求 $CPL \ge DPL$（$RPL$ 不参加检查），且转移后，$CPL$ 仍然保持原值。
综上所述，直接访问代码段并不引起当前特权级的迁移。

间接访问代码段通过门（Gate）来实现。
门是一种特殊的段，可细分为调用门（Call Gate）、任务门（Task Gate）、中断门（Interupt Gate）、陷阱门（Trap Gate）四种。
不同于直接转移指令，通过门机制可以实现特权级的迁移。
限于篇幅，本节仅简要介绍调用门和中断门。


\lstinline{jmpf} 和 \lstinline{callf} 这两条指令的操作数由段选择子和段内偏移地址构成。
前文已经提及过，这两条转移指令无法将执行流直接转移到另一特权级不同的非一致性代码段（不满足条件 $CPL = DPL \ge RPL$）。
为此，x86 体系结构中引入了调用门。
    
使用调用门时，转移请求发起方仍通过上述两种转移指令发起转移请求，与一般的远转移操作不同的是，指令操作数中的选择子设置为调用门选择子，段内偏移地址操作数被忽略。
调用门选择子指向的段类型为调用门（图 \ref{CALL_GATE}）。
CPU 会先根据调用门选择子在 GDT（或 LDT） 中找到相应的调用门描述符，调用门描述符中则保存目标段选择子，CPU 根据目标段选择子在 GDT（或 LDT）中找到目标段描述符，根据目标段描述符中指示的目标段基址跳转到目标代码段指行。
门的特点就在于其要求当前代码段特权级不高于目标代码段特权级，即特权级只能自低向高迁移。

\begin{figure}[h!]
    \Centering
    \caption{调用门}
    \label{CALL_GATE}
    \includegraphics[scale=0.75]{build/Paper/Assets/CallGate.pdf}
\end{figure}

在 x86 体系结构下，从高特权级转移到低特权级只有返回指令一种方式，可细分为调用门所使用的远返回指令 \lstinline{retf} 和其余三种门使用中断返回指令 \lstinline{iret}。

\subsubsection{中断发生时的特权级检查和栈切换}

调用门严重依赖于 GDT（或 LDT），且只能由程序主动发起调用请求。
相比之下，任务门、中断门、陷阱门的描述符则存放于中断描述符表中，中断既可以由程序主动发起（软中断、异常），也可以由外部事件触发（外中断、不可屏蔽中断）。
前文已经介绍过，每个中断或异常事件都被指派一个固定的中断向量号。
与调用门类似，中断门（图 \ref{INT_GATE}）要求当前代码段特权级不高于目标代码段特权级，进而允许特权级自低向高迁移。

\begin{figure}[h!]
    \Centering
    \caption{中断门}
    \label{INT_GATE}
    \includegraphics[scale=0.75]{build/Paper/Assets/IntGate.pdf}
\end{figure}

不同的特权级都拥有独立的栈，以避免不同特权级共用一个栈导致数据污染、栈溢出等诸多问题。
当特权级发生迁移时，栈也需要一并切换。

对于中断前执行流特权级与目标特权级相同的情况，中断发生时不发生栈的切换（图 \ref{INT_WITHOUT_STACK_SWITCH}）。
CPU 在当前栈上保存 \lstinline{EFLAGS}、\lstinline{CS}、\lstinline{EIP} 及错误码（如有的话）。

\begin{figure}[h!]
    \Centering
    \caption{中断发生时无栈切换（同特权级）}
    \label{INT_WITHOUT_STACK_SWITCH}
    \includegraphics[scale=0.75]{build/Paper/Assets/IntWithoutStackSwitch.pdf}
\end{figure}

x86 处理器中有一个特殊的寄存器 \lstinline{TR}，\lstinline{TR} 中保存一类特殊的数据结构任务状态段（Task State Segment，TSS）的选择子（图 \ref{TSS}），TSS 以段描述符的形式在 GDT 中注册。
TSS 用于描述一项任务，进而在处理器层面实现多任务的管理。
当处理器需要进行任务切换时，只需要将 \lstinline{TR} 修改为下一项任务对应 TSS 的选择子。
由于这种任务切换方式开销较大，且任务切换方式不够灵活，因此在现代操作系统中并未采用该方案进行任务切换。
尽管如此，处理器在发生特权级切换时仍然需要使用到 TSS 中的字段。

\begin{figure}[h!]
    \Centering
    \caption{TR、GDT、TSS 三者关系}
    \label{TSS}
    \includegraphics[scale=0.75]{build/Paper/Assets/TSS.pdf}
\end{figure}

见图 \ref{INT_WITH_STACK_SWITCH}，用户程序在 Ring 3 执行，中断事件发生后，CPU 从 TSS 中取出 \lstinline{SS0} 用做内核栈的选择子，取出 \lstinline{ESP0} 作为内核栈的栈顶指针。
随后，CPU 将中断发生前的 \lstinline{SS} 和 \lstinline{ESP} 压入内核栈中，然后才将 \lstinline{EFLAGS}、\lstinline{CS}、\lstinline{EIP} 及错误码（如有的话）压栈。

\begin{figure}[h!]
    \Centering
    \caption{中断发生时有栈切换（不同特权级）}
    \label{INT_WITH_STACK_SWITCH}
    \includegraphics[scale=0.75]{build/Paper/Assets/IntWithStackSwitch.pdf}
\end{figure}

中断返回时，在弹出 \lstinline{CS} 时也会执行特权级检查，若发现 \lstinline{CS.CPL = 3}，则还会将 \lstinline{SS} 和 \lstinline{ESP} 弹出。
TSS 仅在中断发生栈切换时使用，因而对于每个处理器核心，只需要在 GDT 中注册一个 TSS，并使 \lstinline{TR} 指向该 TSS。
在后续章节实现线程时，由于每个线程都有不同的内核栈，因而在调度新的线程取代旧的线程上处理器运行时，TSS 中的 \lstinline{SS0} 和 \lstinline{ESP0} 也需要同步修改，具体细节在本节不作展开。

